﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace GDM.Hogar.Soporte.Helpers
{
    public static class Conversor
    {
        private const int Istrue = 1;

        private static readonly ConcurrentDictionary<Type, Delegate> CacheExpresion = new ConcurrentDictionary<Type, Delegate>();

        public static IList<T> ConvertirA<T>(IDataReader lectorDatos)
        {
            var lista = new List<T>();
            Func<IDataReader, T> crearEntidad = CrearEntidad<T>();

            while (lectorDatos.Read())
            {
                lista.Add(crearEntidad(lectorDatos));
            }
            return lista;
        }

        public static Pair<IList<T>, IList<TV>> ConvertirA<T, TV>(IDataReader lectorDatos)
        {
            var primerLista = new List<T>();
            Func<IDataReader, T> crearPrimerEntidad = CrearEntidad<T>();

            while (lectorDatos.Read())
            {
                primerLista.Add(crearPrimerEntidad(lectorDatos));
            }

            var segundaLista = new List<TV>();
            Func<IDataReader, TV> crearSegundaLista = CrearEntidad<TV>();

            if (lectorDatos.NextResult())
            {
                while (lectorDatos.Read())
                {
                    segundaLista.Add(crearSegundaLista(lectorDatos));
                }
            }

            return new Pair<IList<T>, IList<TV>> { First = primerLista, Second = segundaLista };
        }

        private static Func<IDataReader, T> CrearEntidad<T>()
        {
            Delegate resDelegado;
            if (!CacheExpresion.TryGetValue(typeof(T), out resDelegado))
            {
                // Obtiene la propiedad indexada del DataReader
                var propIndexada = typeof(IDataRecord).GetProperty("Item", new[] { typeof(string) });

                // Lista de sentencias del metodo dinamico
                var sentencias = new List<Expression>();

                // Tipo de instancia de la entidad
                ParameterExpression paramInstancia = Expression.Variable(typeof(T));

                // Parametro para el DataReader
                ParameterExpression paramLectorDatos = Expression.Parameter(typeof(IDataReader));

                // Crea y asigna el nuevo tipo T a la variable
                BinaryExpression crearInstancia = Expression.Assign(paramInstancia, Expression.New(typeof(T)));
                sentencias.Add(crearInstancia);

                IList<PropertyInfo> propiedades = typeof(T).GetProperties().Where(p => p.CanWrite && !p.PropertyType.IsInterface).ToList();
                foreach (var propiedad in propiedades)
                {
                    // propiedad de la instancia
                    MemberExpression obtenerPropiedad = Expression.Property(paramInstancia, propiedad);

                    // row[propiedad] Los nombres de columna son iguales a los nombres de propiedad del tipo T
                    IndexExpression obtenerValor = Expression.MakeIndex(paramLectorDatos, propIndexada, new[] { Expression.Constant(propiedad.Name) });

                    BinaryExpression asignarPropiedad = ObtenerValor(propiedad, obtenerPropiedad, obtenerValor);

                    if (asignarPropiedad != null)
                        sentencias.Add(asignarPropiedad);
                }

                var sentenciaRetorno = paramInstancia;
                sentencias.Add(sentenciaRetorno);

                var cuerpo = Expression.Block(paramInstancia.Type, new[] { paramInstancia }, sentencias.ToArray());
                var lambda = Expression.Lambda<Func<IDataReader, T>>(cuerpo, paramLectorDatos);
                resDelegado = lambda.Compile();

                // Cache the dynamic method into ExpressionCache dictionary
                CacheExpresion[typeof(T)] = resDelegado;
            }

            return (Func<IDataReader, T>)resDelegado;
        }

        private static BinaryExpression ObtenerValor(PropertyInfo propiedad, MemberExpression obtenerPropiedad, IndexExpression obtenerValor)
        {
            BinaryExpression asignarPropiedad;

            if (propiedad.PropertyType == typeof(bool?))
            {
                asignarPropiedad = Expression.Assign(obtenerPropiedad, Expression.TypeAs(obtenerValor, propiedad.PropertyType));
            }
            else
            {
                if (propiedad.PropertyType == typeof(bool))
                {
                    asignarPropiedad = Expression.Assign(obtenerPropiedad, Expression.Equal(Expression.Convert(obtenerValor, typeof(int)), Expression.Constant(Istrue)));
                }
                else
                {
                    if (propiedad.PropertyType == typeof(int?) || propiedad.PropertyType == typeof(decimal?) ||
                        propiedad.PropertyType == typeof(DateTime?) || propiedad.PropertyType == typeof(long?))
                    {
                        asignarPropiedad = Expression.Assign(obtenerPropiedad, Expression.TryCatch(Expression.Convert(obtenerValor, propiedad.PropertyType), Expression.Catch(typeof(Exception), Expression.TypeAs(Expression.Constant(null), propiedad.PropertyType))));
                    }
                    else
                    {
                        asignarPropiedad = Expression.Assign(obtenerPropiedad, Expression.Convert(obtenerValor, propiedad.PropertyType));
                    }
                }
            }

            return asignarPropiedad;
        }
    }
}